package jwanalytics.utils.date;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

/** 
 * Quite often, with web analytics we will be dealing with dates, number of days in between two dates, etc.
 * This class has a number of date methods not present in the Java API and needed for such analytics.
 * 
 * Please send new method requests for inclusion to the developer list.
 */
public class BaseCalendar {
	
	/** Number of seconds in one hour. */
	public static final int MINUTES_IN_ONE_HOUR = 60;    
	
	/** One hour in milliseconds. */
    static final double ONE_HOUR = MINUTES_IN_ONE_HOUR * 60 * 1000L;

	/** One day in milliseconds. */
    static final double ONE_DAY = 24 * ONE_HOUR;	
        
    /** Just to avoid errors by daylight savings time. */
	static final GregorianCalendar C11AM = new GregorianCalendar(1900, 4, 5, 11, 00, 00); // May 5	    
    
	/** @return true if dates are on the dame day/month/year. */
	public static boolean areAtTheSameDay(Date d1, Date d2) {
		return makeItFirstSecondOfDay(d1).equals(makeItFirstSecondOfDay(d2));
	}
		
	/** 
	 * Brings the hour to the first second of the day passed as argument.
	 * @param toSetToFirstSecond Not altered.  A new date is created that
	 * has the same day, but hour is set to 0:00:00.
	 * @return Same day as the argument but with hour set to 0:00:00
	 */
	public static Date makeItFirstSecondOfDay(Date toSetToFirstSecond) {
		GregorianCalendar cal = new GregorianCalendar();
		cal.setTime(toSetToFirstSecond);
		cal.set(Calendar.HOUR_OF_DAY, 0);
		cal.getTime(); //it is a bug on JDK 1.5 this needs to be called or it does not work.
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		cal.set(Calendar.MILLISECOND, 0); //if not set dates will not be equal. Was a bug.
		cal.set(Calendar.AM_PM, Calendar.AM);
		return cal.getTime();
	}
	
	/** 
	 * This is not the difference in days, but days span, or
	 * in other words how many days the interval touches.
	 * 
	 * @return Example an interval from Monday 23:00 to Wednesday 1:00 (AM) will touch 3 days (Monday, Tuesday, Wednesday)
	 * This method would return 3 for this interval as its daysSpan value.
	 */
	public static int daysSpan(Date date1, Date date2) {
		if (areAtTheSameDay(date1, date2)) {
			return 1;
		}
		Date tmp;
		if (!date1.before(date2)) {
			tmp = date1;
			date1 = date2;
			date2 = tmp;
		}
		Date d1 = makeItFirstSecondOfDay(date1);
		Date d2 = setHour(date2, C11AM.getTime()); //important to avoid errors by daylight savings time. 
		double duration = (d2.getTime() - d1.getTime());
		return (int)((duration + ONE_HOUR) / ONE_DAY)+1; //one hour to account for daylight savings time.
	}

	
	/**
 	 * Adds a certain number of days to a date.  Will take into account months with less days even
 	 * leap years. Reuses/delegates to addDays(GregorianCalendar cal, int days).
 	 * @see addDays(GregorianCalendar cal, int days).
 	 */
	public static Date addDays(Date date1, int days) {
		GregorianCalendar cal = new GregorianCalendar();
		cal.setTime(date1);
		return addDays(cal, days).getTime();
	}
	
	/**
	 * Adds a certain number of days to a date.
	 * @param cal Calendar set to a particular date.
	 * @param days Days to be added.
	 * @return
	 */
	public static GregorianCalendar addDays(GregorianCalendar cal, int days) {
		cal.setLenient(true);
		cal.set(Calendar.DAY_OF_MONTH, cal.get(Calendar.DAY_OF_MONTH)+days);
		return cal;		
	}
	
	/** 
	 * @return True if it is a weekend day (Sat or Sun). This is done by testing whether this is not a week day, leaving weekend as the complementary set of possible values. 
	 * @see isWeekDay
	 */
	public static boolean isWeekend(Date dateToTest) {
		return !isWeekDay(dateToTest);
	}
	
	
	/** @return True if it is a week day (Mon til Fri). */
	public static boolean isWeekDay(Date dateToTest) {
		GregorianCalendar dt = new GregorianCalendar(); // today
		dt.setTime(dateToTest);
		int dayOfWeek = dt.get(Calendar.DAY_OF_WEEK);
		if (dayOfWeek != Calendar.SUNDAY && dayOfWeek != Calendar.SATURDAY)
			return true;
		else 
			return false;
	}
	
	
	/** 
	 * @return True if it is a week day (Mon-Fri). 
	 * Use Calendar.MONDAY... 
	 */
	public static boolean isAParticularWeekDay(Date dateToTest, int weekday) {
		GregorianCalendar dt = new GregorianCalendar(); // today
		dt.setTime(dateToTest);
		int dayOfWeek = dt.get(Calendar.DAY_OF_WEEK);
		if (dayOfWeek == weekday)
			return true;
		else 
			return false;
	}
	
	
	/**
	 * See junit test. 
	 * @return Return the next Monday. 
	 * @param typeweekday Use Calendar.MONDAY, Calendar.TUESDAY for second argument. 
	 */
	public static Date nextWeekdayOfType(Date dateToTest, int typeweekday) {
		GregorianCalendar dt = new GregorianCalendar(); // today
		dt.setTime(dateToTest);
		int dayOfWeek = dt.get(Calendar.DAY_OF_WEEK);
		int diff = 7-(dayOfWeek - typeweekday);
		if (diff >= 8) diff = diff - 7; //more robust expression than if (diff == 8) diff = 1;
		return addDays(dateToTest, diff);
	}
	
	/** 
	 * Calculates the difference in days between two dates. Could be wrong by 1 hour. Take care when using. 
	 * It is approximate because it does not count daylight savings time. 
	 * @return Difference value in milliseconds precision.
	 */
	public static int approximateDifferenceInDays(Date date1, Date date2) {
		//
		Date tmp = null;
		if (!date1.before(date2)) {
			tmp = date1;
			date1 = date2;
			date2 = tmp;
		}
		double duration = (date2.getTime() - date1.getTime());
		double days = (duration / ONE_DAY);
	    return (int)days;
	}	
	
	/**
	 * Examples: 
	 * city=New York; state=NY; country=US (A city specific holiday in New York, eg. city anniversary).
	 * city=null; state=null; country=NL (A national holiday in The Netherlands).
	 * This method is not implemented yet and will be ready on an upcoming ontology/metadata based 
	 * version of wicket analytics.
	 * @throws Exception */
	public static boolean isHoliday(Date dt, String city, String state, String country) throws Exception {
		//On this project we return false for holiday for any date.
		//On the upcoming wicket analytics metadata project we will add an ontology
		//knowledge base which will contain knowledge on holidays, browsers, etc.
		//This ontology will be used to better generate dimensions or update dimensions
		//such as date and browser.
		return false;
	}
	
	
	/** 
	 * Sets the hour of a particular date as a copy of the hour of a second date. 
	 * @param date Date to set the hour from, returned.
	 * @param hour to copy from.  
	 */
	public static Date setHour(Date date, Date hour) {
		Calendar cal = new GregorianCalendar();
		cal.setTime(date);
		Calendar cal2 = new GregorianCalendar();
		cal2.setTime(hour);
		cal.set(Calendar.HOUR_OF_DAY, cal2.get(Calendar.HOUR_OF_DAY));
		cal.set(Calendar.MINUTE, cal2.get(Calendar.MINUTE));		
		cal.set(Calendar.SECOND, cal2.get(Calendar.SECOND));
		return cal.getTime();
	}
	
	
	/** 
	 * Day number/index since the start of the analytics. This is the same as the integer index on the date_dimension table (date_key column). 
	 * The start of analytics is configured on ejb-jar.xml and on the java program to populate the dimensional model.
	 * @throws Exception 
	 */
	public static int dayIndex(Date dt, Date startDate) {
		return BaseCalendar.daysSpan(dt, startDate);
	}
	
	
	/** 
	 * Index on the time_of_day_dimension table. */
	public static int timeIndex(Date dt) {
		GregorianCalendar gc = new GregorianCalendar();
		gc.setTime(dt);
		int hour = gc.get(Calendar.HOUR_OF_DAY);
		int minute = gc.get(Calendar.MINUTE);		
		return (hour * MINUTES_IN_ONE_HOUR + minute);
	}	
	
}
